/*
 * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.dtv.lwuit;

import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.Hashtable;

import org.thenesis.microbackend.ui.graphics.VirtualGraphics;
import org.thenesis.microbackend.ui.graphics.VirtualImage;

import com.sun.dtv.lwuit.geom.Dimension;
import com.sun.dtv.lwuit.util.Log;

/**
 * Abstracts the underlying platform images allowing us to treat them as a
 * uniform object.
 * 
 * @author Chen Fishbein
 */
public class Image {

    private VirtualImage image;
    private int transform;

    private boolean opaqueTested = false;
    private boolean opaque;
    private Graphics g;
    private Hashtable scaleCache;

    /** Creates a new instance of ImageImpl */
    Image(VirtualImage image) {
        this.image = image;
    }

    /** Creates a new instance of ImageImpl */
    Image(int[] imageArray, int w, int h) {
        this(new VirtualImage(imageArray, w, h, true));
    }

    /**
     * Returns a cached scaled image
     * 
     * @param size
     *            the size of the cached image
     * @return cached image
     */
    Image getCachedImage(Dimension size) {
        if (scaleCache != null) {
            WeakReference w = (WeakReference) scaleCache.get(size);
            if (w != null) {
                return (Image) w.get();
            }
        }
        return null;
    }

    /**
     * Returns a cached scaled image
     * 
     * @param size
     *            the size of the cached image
     * @return cached image
     */
    void cacheImage(Dimension size, Image i) {
        if (scaleCache == null) {
            scaleCache = new Hashtable();
        }
        WeakReference w = new WeakReference(i);
        scaleCache.put(size, w);
    }

    /**
     * Extracts a subimage from the given image allowing us to breakdown a
     * single large image into multiple smaller images in RAM, this actually
     * creates a standalone version of the image for use.
     * 
     * @param width
     *            the width of internal images
     * @param height
     *            the height of internal images
     * @return An array of all the possible images that can be created from the
     *         source
     * @throws java.io.IOException
     */
    public Image subImage(int x, int y, int width, int height, boolean processAlpha) {
        // we use the getRGB API rather than the mutable image API to allow
        // translucency to
        // be maintained in the newly created image
        int[] arr = new int[width * height];
        image.getRGB(arr, 0, width, x, y, width, height);

        Image i = new Image(new VirtualImage(arr, width, height, processAlpha));
        i.opaque = opaque;
        i.opaqueTested = opaqueTested;
        return i;
    }

    /**
     * Returns an instance of this image rotated by the given number of degrees.
     * By default 90 degree angle divisions are supported, anything else is
     * implementation dependent. This method assumes a square image. Notice that
     * it is inefficient in the current implementation to rotate to non-square
     * angles,
     * <p>
     * E.g. rotating an image to 45, 90 and 135 degrees is inefficient. Use
     * rotatate to 45, 90 and then rotate the 45 to another 90 degrees to
     * achieve the same effect with less memory.
     * 
     * @param degrees
     *            A degree in right angle must be larer than 0 and up to 359
     *            degrees
     * @return new image instance with the closest possible rotation
     */
    public Image rotate(int degrees) {

        if (Log.TRACE_ENABLED)
            System.out.println("[DEBUG] Image.rotate(): not implemented yet");

        return null;

        // // a square angle so we can use the "fast" algorithm...
        // int transform = 0;
        // Image i;
        // if(degrees % 90 == 0) {
        // transform = fastRotate(degrees);
        // i = new Image(image);
        // } else {
        // // rotate up to the point to a smaller than 90 degree angle then let
        // the radian code do its magic...
        // if(degrees > 90) {
        // int deg = degrees - degrees % 90;
        // transform = fastRotate(deg);
        // degrees -= deg;
        // }
        // // can't use sprite rotation since it will lose transparency
        // information...
        // int width = image.getWidth();
        // int height = image.getHeight();
        // int[] arr = new int[width * height];
        // int[] dest = new int[arr.length];
        // image.getRGB(arr, 0, width, 0, 0, width, height);
        // int centerX = width / 2;
        // int centerY = height / 2;
        //
        // double radians = Math.toRadians(-degrees);
        // double cosDeg = Math.cos(radians);
        // double sinDeg = Math.sin(radians);
        // for(int x = 0 ; x < width ; x++) {
        // for(int y = 0 ; y < height ; y++) {
        // int x2 = round(cosDeg * (x - centerX) - sinDeg * (y - centerY) +
        // centerX);
        // int y2 = round(sinDeg * (x - centerX) + cosDeg * (y - centerY) +
        // centerY);
        // if(!(x2 < 0 || y2 < 0 || x2 >= width || y2 >= height)) {
        // int destOffset = x2 + y2 * width;
        // if(destOffset >= 0 && destOffset < dest.length) {
        // dest[x + y * width] = arr[destOffset];
        // }
        // }
        // }
        // }
        // i = new Image(VirtualImage.createRGBImage(dest, width, height,
        // true));
        // }
        //        
        // i.transform = transform;
        // return i;
    }

    private int round(double d) {
        double f = Math.floor(d);
        double c = Math.ceil(d);
        if (c - d < d - f) {
            return (int) c;
        }
        return (int) f;
    }

    /**
     * Creates a new image instance with the alpha channel of opaque/translucent
     * pixels within the image using the new alpha value. Transparent (alpha ==
     * 0) pixels remain transparent. All other pixels will have the new alpha
     * value.
     * 
     * @param alpha
     *            New value for the entire alpha channel
     * @return Translucent/Opaque image based on the alpha value and the pixels
     *         of this image
     */
    public Image modifyAlpha(byte alpha) {
        int w = image.getWidth();
        int h = image.getHeight();
        int size = w * h;
        int[] arr = new int[size];
        image.getRGB(arr, 0, w, 0, 0, w, h);
        int alphaInt = (((int) alpha) << 24) & 0xff000000;
        for (int iter = 0; iter < size; iter++) {
            if ((arr[iter] & 0xff000000) != 0) {
                arr[iter] = (arr[iter] & 0xffffff) | alphaInt;
            }
        }
        Image i = new Image(arr, w, h);
        i.opaqueTested = true;
        i.opaque = false;
        return i;
    }

    /**
     * Creates a new image instance with the alpha channel of opaque/translucent
     * pixels within the image using the new alpha value. Transparent (alpha ==
     * 0) pixels remain transparent. All other pixels will have the new alpha
     * value.
     * 
     * @param alpha
     *            New value for the entire alpha channel
     * @param removeColor
     *            pixels matching this color are made transparent (alpha channel
     *            ignored)
     * @return Translucent/Opaque image based on the alpha value and the pixels
     *         of this image
     */
    public Image modifyAlpha(byte alpha, int removeColor) {
        removeColor = removeColor & 0xffffff;
        int w = image.getWidth();
        int h = image.getHeight();
        int size = w * h;
        int[] arr = new int[size];
        image.getRGB(arr, 0, w, 0, 0, w, h);
        int alphaInt = (((int) alpha) << 24) & 0xff000000;
        for (int iter = 0; iter < size; iter++) {
            if ((arr[iter] & 0xff000000) != 0) {
                arr[iter] = (arr[iter] & 0xffffff) | alphaInt;
                if (removeColor == (0xffffff & arr[iter])) {
                    arr[iter] = 0;
                }
            }
        }
        Image i = new Image(arr, w, h);
        i.opaqueTested = true;
        i.opaque = false;
        return i;
    }

    /**
     * Returns a mirror instance of this image
     * 
     * @return a mirror instance of this image
     * @deprecated this method is no longer supported due to issues when mixing
     *             it with other methods such as rotate. Future versions of the
     *             API will provide an alternative
     */
    public Image mirror() {

        if (Log.TRACE_ENABLED)
            System.out.println("[DEBUG] Image.mirror(): not implemented yet");
        return null;

        // Image i = new Image(image);
        // if(transform != 0) {
        // switch(transform) {
        // case Sprite.TRANS_MIRROR:
        // i.transform = 0;
        // break;
        // case Sprite.TRANS_MIRROR_ROT180:
        // i.transform = Sprite.TRANS_ROT180;
        // break;
        // case Sprite.TRANS_MIRROR_ROT270:
        // i.transform = Sprite.TRANS_ROT270;
        // break;
        // case Sprite.TRANS_MIRROR_ROT90:
        // i.transform = Sprite.TRANS_ROT90;
        // break;
        // case Sprite.TRANS_NONE:
        // i.transform = Sprite.TRANS_MIRROR;
        // break;
        // case Sprite.TRANS_ROT180:
        // i.transform = Sprite.TRANS_MIRROR_ROT180;
        // break;
        // case Sprite.TRANS_ROT270:
        // i.transform = Sprite.TRANS_MIRROR_ROT270;
        // break;
        // case Sprite.TRANS_ROT90:
        // i.transform = Sprite.TRANS_MIRROR_ROT90;
        // break;
        // default:
        // throw new
        // IllegalStateException("Image could not be mirrored, error code: " +
        // transform);
        // }
        // } else {
        // i.transform = transform;
        // }
        // return i;
    }

    // /**
    // * Rotates the given image array fast assuming this is a "right" angle
    // * divides by 90 degrees
    // */
    // private int fastRotate(int degrees) {
    // // if we have a preexisting rotation...
    // switch(transform) {
    // case javax.microedition.lcdui.game.Sprite.TRANS_ROT90:
    // degrees += 90;
    // break;
    // case javax.microedition.lcdui.game.Sprite.TRANS_ROT180:
    // degrees += 180;
    // break;
    // case javax.microedition.lcdui.game.Sprite.TRANS_ROT270:
    // degrees += 270;
    // break;
    // }
    //        
    // switch((degrees % 360) / 90) {
    // case 0:
    // return 0;
    //            
    // // 90 degree angle
    // case 1:
    // return javax.microedition.lcdui.game.Sprite.TRANS_ROT90;
    //
    // // 180 degree angle
    // case 2:
    // return javax.microedition.lcdui.game.Sprite.TRANS_ROT180;
    //            
    // // 270 degree angle
    // case 3:
    // return javax.microedition.lcdui.game.Sprite.TRANS_ROT270;
    //
    // default:
    // throw new IllegalArgumentException("Fast rotate can't handle degrees = "
    // + degrees);
    // }
    // }

    /**
     * Converts a newly loaded masked image into a translucent image. A masked
     * image is an image file of even height that is two separate opaque images
     * the top part is the actual image containing the opaque content. The
     * bottom part is a mask that would be ovelayed as the alpha channel of the
     * top part of the image. The red channel of the bottom part would just be
     * assigned to the alpha portion of the top part thus allowing an image to
     * contain translucency while being portable to different devices.
     * 
     * @return Translucent image instance
     * @deprecated translucency masks are no longer supported or necessary, use
     *             resources for portability and unification
     */
    public Image extractTranslucentMask() {

        if (Log.TRACE_ENABLED)
            System.out.println("[DEBUG] Image.extractTranslucentMask(): not implemented yet");

        return null;

        // int w = image.getWidth();
        // int h = image.getHeight();
        // int[] arr = new int[w * h];
        // image.getRGB(arr, 0, w, 0, 0, w, h);
        // int offset = h * w / 2;
        // for(int iter = 0 ; iter < offset ; iter++) {
        // // extract the red component from the bottom portion of the image
        // int alpha = arr[iter + offset] & 0xff0000;
        //            
        // // shift the alpha 8 bits to the left
        // // apply the alpha to the top portion of the image
        // arr[iter] = (arr[iter] & 0xffffff) | (alpha << 8);
        // }
        // return new Image(VirtualImage.createRGBImage(arr, w, h / 2, true));
    }

    /**
     * creates an image from the given path based on MIDP's createImage(path)
     * 
     * @param path
     * @throws java.io.IOException
     * @return newly created image object
     */
    public static Image createImage(String path) throws IOException {
        try {
            InputStream is = Image.class.getResourceAsStream(path);
            return new Image(new VirtualImage(is));
        } catch (OutOfMemoryError err) {
            // Images have a major bug on many phones where they sometimes throw
            // an OOM with no reason. A system.gc followed by the same call over
            // solves the problem. This has something to do with the fact that
            // there is no Image.dispose method in existance.
            System.gc();
            System.gc();
            InputStream is = Image.class.getResourceAsStream(path);
            return new Image(new VirtualImage(is));
        }
    }

    /**
     * creates an image from an InputStream
     * 
     * @param stream
     *            a given InputStream
     * @throws java.io.IOException
     */
    public static Image createImage(InputStream stream) throws IOException {
        try {
            return new Image(new VirtualImage(stream));
        } catch (OutOfMemoryError err) {
            // Images have a major bug on many phones where they sometimes throw
            // an OOM with no reason. A system.gc followed by the same call over
            // solves the problem. This has something to do with the fact that
            // there is no Image.dispose method in existance.
            System.gc();
            System.gc();
            return new Image(new VirtualImage(stream));
        }
    }

    /**
     * creates an image from an RGB image
     * 
     * @param rgb
     *            the RGB image array data
     * @param width
     *            the image width
     * @param height
     *            the image height
     * @return an image from an RGB image
     */
    public static Image createImage(int[] rgb, int width, int height) {
        try {
            Image i = new Image(new VirtualImage(rgb, width, height, true));

            // its already set so might as well do the test
            i.testOpaque(rgb);
            return i;
        } catch (OutOfMemoryError err) {
            // Images have a major bug on many phones where they sometimes throw
            // an OOM with no reason. A system.gc followed by the same call over
            // solves the problem. This has something to do with the fact that
            // there is no Image.dispose method in existance.
            System.gc();
            System.gc();
            return new Image(new VirtualImage(rgb, width, height, true));
        }
    }

    /**
     * creates and buffered Image
     * 
     * @param width
     *            the image width
     * @param height
     *            the image height
     * @return an image in a given width and height dimension
     */
    public static Image createImage(int width, int height) {
        try {
            return new Image(new VirtualImage(width, height));
        } catch (OutOfMemoryError err) {
            // Images have a major bug on many phones where they sometimes throw
            // an OOM with no reason. A system.gc followed by the same call over
            // solves the problem. This has something to do with the fact that
            // there is no Image.dispose method in existance.
            System.gc();
            System.gc();
            return new Image(new VirtualImage(width, height));
        }
    }

    /**
     * creates an image from a given byte array data
     * 
     * @param bytes
     *            the array of image data in a supported image format
     * @param offset
     *            the offset of the start of the data in the array
     * @param len
     *            the length of the data in the array
     */
    public static Image createImage(byte[] bytes, int offset, int len) {
        try {
            try {
                return new Image(new VirtualImage(bytes, offset, len));
            } catch (OutOfMemoryError err) {
                // Images have a major bug on many phones where they sometimes
                // throw
                // an OOM with no reason. A system.gc followed by the same call
                // over
                // solves the problem. This has something to do with the fact
                // that
                // there is no Image.dispose method in existance.
                System.gc();
                System.gc();
                return new Image(new VirtualImage(bytes, offset, len));
            }
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * If this is a mutable image a grpahics object allowing us to draw on it is
     * returned.
     * 
     * @return Graphics object allowing us to manipulate the content of a
     *         mutable image
     */
    public Graphics getGraphics() {
        if (g == null) {
            g = new Graphics();
            g.setGraphics(image.getGraphics());
        }
        return g;
    }

    /**
     * Returns the width of the image
     * 
     * @return the width of the image
     */
    public int getWidth() {
        return image.getWidth();
    }

    /**
     * Returns the height of the image
     * 
     * @return the height of the image
     */
    public int getHeight() {
        return image.getHeight();
    }

    void drawImage(Graphics g, int x, int y) {
        g.drawImage(image, x, y, transform);
    }

    /**
     * Obtains ARGB pixel data from the specified region of this image and
     * stores it in the provided array of integers. Each pixel value is stored
     * in 0xAARRGGBB format, where the high-order byte contains the alpha
     * channel and the remaining bytes contain color components for red, green
     * and blue, respectively. The alpha channel specifies the opacity of the
     * pixel, where a value of 0x00 represents a pixel that is fully transparent
     * and a value of 0xFF represents a fully opaque pixel. The rgb information
     * contained within the image, this method ignors rotation and mirroring in
     * some/most situations and cannot be used in such cases.
     * 
     * @param rgbData
     *            an array of integers in which the ARGB pixel data is stored
     * @param offset
     *            the index into the array where the first ARGB value is stored
     * @param scanlength
     *            the relative offset in the array between corresponding pixels
     *            in consecutive rows of the region
     * @param x
     *            the x-coordinate of the upper left corner of the region
     * @param y
     *            the y-coordinate of the upper left corner of the region
     * @param width
     *            the width of the region
     * @param height
     *            the height of the region
     */
    void getRGB(int[] rgbData, int offset, int scanlength, int x, int y, int width, int height) {
        image.getRGB(rgbData, offset, scanlength, x, y, width, height);
    }

    /**
     * Extracts data from this image into the given RGBImage
     * 
     * @param image
     *            RGBImage that would receive pixel data
     * @param destX
     *            x location within RGBImage into which the data will be written
     * @param destY
     *            y location within RGBImage into which the data will be written
     * @param x
     *            location within the source image
     * @param y
     *            location within the source image
     * @param width
     *            size of the image to extract from the source image
     * @param height
     *            size of the image to extract from the source image
     */
    public void toRGB(RGBImage image, int destX, int destY, int x, int y, int width, int height) {
        getRGB(image.getRGB(), destX * destY, width, x, y, width, height);
    }

    /**
     * Returns the content of this image as a newly created ARGB array.
     * 
     * @return new array instance containing the ARGB data within this image
     */
    public int[] getRGB() {
        int width = getWidth();
        int height = getHeight();
        int[] rgbData = new int[width * height];
        getRGB(rgbData, 0, width, 0, 0, width, height);
        return rgbData;
    }

    /**
     * Scales the image to the given width while updating the height based on
     * the aspect ratio of the width
     * 
     * @param width
     *            the given new image width
     */
    public Image scaledWidth(int width) {
        float ratio = ((float) width) / ((float) getWidth());
        return scaled(width, (int) (getHeight() * ratio));
    }

    /**
     * Scales the image to the given height while updating the width based on
     * the aspect ratio of the height
     * 
     * @param height
     *            the given new image height
     */
    public Image scaledHeight(int height) {
        float ratio = ((float) height) / ((float) getHeight());
        return scaled((int) (getWidth() * ratio), height);
    }

    /**
     * Scales the image while mainting the aspect ratio to the smaller size
     * image
     * 
     * @param width
     *            the given new image width
     * @param height
     *            the given new image height
     */
    public Image scaledSmallerRatio(int width, int height) {
        float hRatio = ((float) height) / ((float) getHeight());
        float wRatio = ((float) width) / ((float) getWidth());
        if (hRatio < wRatio) {
            return scaled(width, (int) (getHeight() * hRatio));
        } else {
            return scaled((int) (getWidth() * wRatio), height);
        }
    }

    /**
     * Returns a scaled version of this image image using the given width and
     * height, this is a fast algorithm that preserves transulcent information
     * 
     * @param width
     *            width for the scaling
     * @param height
     *            height of the scaled image
     * @return new image instance scaled to the given height and width
     */
    public Image scaled(int width, int height) {
        if (width == image.getWidth() && height == image.getHeight()) {
            return this;
        }
        Dimension d = new Dimension(width, height);
        Image i = getCachedImage(d);
        if (i != null) {
            return i;
        }
        i = new Image(this.image);
        i.scale(width, height);
        i.transform = this.transform;
        cacheImage(d, i);
        return i;
    }

    /**
     * Scale the image to the given width and height, this is a fast algorithm
     * that preserves transulcent information
     * 
     * @param width
     *            width for the scaling
     * @param height
     *            height of the scaled image
     * 
     * @deprecated scale should return an image rather than modify the image in
     *             place use scaled(int, int) instead
     */
    public void scale(int width, int height) {
        int srcWidth = image.getWidth();
        int srcHeight = image.getHeight();

        // no need to scale
        if (srcWidth == width && srcHeight == height) {
            return;
        }

        int[] currentArray = new int[srcWidth];
        int[] destinationArray = new int[width * height];
        opaque = scaleArray(srcWidth, srcHeight, height, width, currentArray, destinationArray);

        // if an image is opaque use a mutable image since on the device it
        // takes
        // far less memory
        // if(opaque) {
        image = new VirtualImage(width, height);
        VirtualGraphics g = image.getGraphics();
        g.drawRGB(destinationArray, 0, width, 0, 0, width, height, false);
        /*
         * } else { image = VirtualImage.createRGBImage(destinationArray, width,
         * height, true); }
         */
    }// resize image

    VirtualImage getImage() {
        return image;
    }

    boolean scaleArray(int srcWidth, int srcHeight, int height, int width, int[] currentArray, int[] destinationArray) {
        // Horizontal Resize
        int yRatio = (srcHeight << 16) / height;
        int xRatio = (srcWidth << 16) / width;
        int xPos = xRatio / 2;
        int yPos = yRatio / 2;

        // if there is more than 16bit color there is no point in using mutable
        // images since they won't save any memory
        boolean opaque = Display.getInstance().numColors() <= 65536;
        for (int y = 0; y < height; y++) {
            int srcY = yPos >> 16;
            getRGB(currentArray, 0, srcWidth, 0, srcY, srcWidth, 1);
            for (int x = 0; x < width; x++) {
                int srcX = xPos >> 16;
                int destPixel = x + y * width;
                if ((destPixel >= 0 && destPixel < destinationArray.length) && (srcX < currentArray.length)) {
                    destinationArray[destPixel] = currentArray[srcX];

                    // if all the pixels have an opaque alpha channel then the
                    // image is opaque
                    opaque = opaque && (currentArray[srcX] & 0xff000000) == 0xff000000;
                }
                xPos += xRatio;
            }
            yPos += yRatio;
            xPos = xRatio / 2;
        }
        this.opaque = opaque;
        return opaque;
    }

    /**
     * Returns true if this is an animated image
     */
    public boolean isAnimation() {
        return false;
    }

    private void testOpaque(int[] rgb) {
        if (!opaqueTested) {
            opaque = true;
            for (int iter = 0; iter < rgb.length; iter++) {
                if ((rgb[iter] & 0xff000000) != 0xff000000) {
                    opaque = false;
                    break;
                }
            }
            opaqueTested = true;
        }
    }

    /**
     * Indicates whether this image is opaque or not
     * 
     * @return true if the image is completely opqaque which allows for some
     *         heavy optimizations
     */
    public boolean isOpaque() {
        if (!opaqueTested) {
            testOpaque(getRGB());
        }
        return opaque;
    }
}
